<!--
@license
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<link rel="import" href="../path.html">
<script>

  Polymer.Bind = {

    // for prototypes (usually)
    prepareModel: function(model) {
      Polymer.Base.mixin(model, this._modelApi);
    },

    _modelApi: {

      _notifyChange: function(source, event, value) {
        value = value === undefined ? this[source] : value;
        event = event || Polymer.CaseMap.camelToDashCase(source) + '-changed';
        this.fire(event, {value: value},
          {bubbles: false, cancelable: false, _useCache:
            Polymer.Settings.eventDataCache || !Polymer.Settings.isIE});
      },

      // TODO(sjmiles): removing _notifyListener from here breaks accessors.html
      // as a standalone lib. This is temporary, as standard/configure.html
      // installs it's own version on Polymer.Base, and we need that to work
      // right now.
      // NOTE: exists as a hook for processing listeners
      /*
      _notifyListener: function(fn, e) {
        // NOTE: pass e.target because e.target can get lost if this function
        // is queued asynchrously
        return fn.call(this, e, e.target);
      },
      */

      // Called from accessors, where effects is pre-stored
      // in the closure for the accessor for efficiency
      _propertySetter: function(property, value, effects, fromAbove) {
        var old = this.__data__[property];
        // NaN is always not equal to itself,
        // if old and value are both NaN we treat them as equal
        // x === x is 10x faster, and equivalent to !isNaN(x)
        if (old !== value && (old === old || value === value)) {
          this.__data__[property] = value;
          if (typeof value == 'object') {
            this._clearPath(property);
          }
          if (this._propertyChanged) {
            this._propertyChanged(property, value, old);
          }
          if (effects) {
            this._effectEffects(property, value, effects, old, fromAbove);
          }
        }
        return old;
      },

      // Called during _applyConfig (well-known downward data-flow hot path)
      // in order to avoid firing notify events
      // TODO(kschaaf): downward bindings (e.g. _applyEffectValue) should also
      // use non-notifying setters but right now that would require looking
      // up readOnly property config in the hot-path
      __setProperty: function(property, value, quiet, node) {
        node = node || this;
        var effects = node._propertyEffects && node._propertyEffects[property];
        if (effects) {
          node._propertySetter(property, value, effects, quiet);
        } else if (node[property] !== value) {
          node[property] = value;
        }
      },

      _effectEffects: function(property, value, effects, old, fromAbove) {
        for (var i=0, l=effects.length, fx; (i<l) && (fx=effects[i]); i++) {
          // always send latest value for property
          // if any previous effect has modified the value, given `value` will be stale
          fx.fn.call(this, property, this[property], fx.effect, old, fromAbove);
        }
      },

      _clearPath: function(path) {
        for (var prop in this.__data__) {
          if (Polymer.Path.isDescendant(path, prop)) {
            this.__data__[prop] = undefined;
          }
        }
      }

    },

    // a prepared model can acquire effects

    ensurePropertyEffects: function(model, property) {
      if (!model._propertyEffects) {
        model._propertyEffects = {};
      }
      var fx = model._propertyEffects[property];
      if (!fx) {
        fx = model._propertyEffects[property] = [];
      }
      return fx;
    },

    addPropertyEffect: function(model, property, kind, effect) {
      var fx = this.ensurePropertyEffects(model, property);
      var propEffect = {
        kind: kind,
        effect: effect,
        fn: Polymer.Bind['_' + kind + 'Effect']
      };
      fx.push(propEffect);
      return propEffect;
    },

    createBindings: function(model) {
      //console.group(model.is);
      // map of properties to effects
      var fx$ = model._propertyEffects;
      if (fx$) {
        // for each property with effects
        for (var n in fx$) {
          // array of effects
          var fx = fx$[n];
          // effects have priority
          fx.sort(this._sortPropertyEffects);
          // create accessors
          this._createAccessors(model, n, fx);
        }
      }
      //console.groupEnd();
    },

    _sortPropertyEffects: (function() {
      // TODO(sjmiles): EFFECT_ORDER buried this way is not ideal,
      // but presumably the sort method is going to be a hot path and not
      // have a `this`. There is also a problematic dependency on effect.kind
      // values here, which are otherwise pluggable.
      var EFFECT_ORDER = {
        'compute': 0,
        'annotation': 1,
        'annotatedComputation': 2,
        'reflect': 3,
        'notify': 4,
        'observer': 5,
        'complexObserver': 6,
        'function': 7
      };
      return function(a, b) {
        return EFFECT_ORDER[a.kind] - EFFECT_ORDER[b.kind];
      };
    })(),

    // create accessors that implement effects

    _createAccessors: function(model, property, effects) {
      var defun = {
        get: function() {
          // TODO(sjmiles): elide delegation for performance, good ROI?
          return this.__data__[property];
        }
      };
      var setter = function(value) {
        this._propertySetter(property, value, effects);
      };
      // ReadOnly properties have a private setter only
      // TODO(kschaaf): Per current Bind factoring, we shouldn't
      // be interrogating the prototype here
      // TODO(sorvell): we want to avoid using `getPropertyInfo` here, but
      // this requires more data in `_propertyInfo`
      var info = model.getPropertyInfo && model.getPropertyInfo(property);
      if (info && info.readOnly) {
        // Computed properties are read-only (no property setter), but also don't
        // need a private setter since they should not be called by the user
        if (!info.computed) {
          model['_set' + this.upper(property)] = setter;
        }
      } else {
        defun.set = setter;
      }
      Object.defineProperty(model, property, defun);
    },

    upper: function(name) {
      return name[0].toUpperCase() + name.substring(1);
    },

    _addAnnotatedListener: function(model, index, property, path, event, negated) {
      if (!model._bindListeners) {
        model._bindListeners = [];
      }
      var fn = this._notedListenerFactory(property, path,
        Polymer.Path.isDeep(path), negated);
      var eventName = event ||
        (Polymer.CaseMap.camelToDashCase(property) + '-changed');
      model._bindListeners.push({
        index: index,
        property: property,
        path: path,
        changedFn: fn,
        event: eventName
      });
    },

    _isEventBogus: function(e, target) {
      return e.path && e.path[0] !== target;
    },

    _notedListenerFactory: function(property, path, isStructured, negated) {
      return function(target, value, targetPath) {
        if (targetPath) {
          var newPath = Polymer.Path.translate(property, path, targetPath);
          this._notifyPath(newPath, value);
        } else {
          // TODO(sorvell): even though we have a `value` argument, we *must*
          // lookup the current value of the property. Multiple listeners and
          // queued events during configuration can theoretically lead to
          // divergence of the passed value from the current value, but we
          // really need to track down a specific case where this happens.
          value = target[property];

          if (negated) {
            value = !value;
          }

          if (!isStructured) {
            this[path] = value;
          } else {
            // TODO(kschaaf): dirty check avoids null references when the object has gone away
            if (this.__data__[path] != value) {
              this.set(path, value);
            }
          }
        }
      };
    },
    // for instances

    prepareInstance: function(inst) {
      inst.__data__ = Object.create(null);
    },

    setupBindListeners: function(inst) {
      var b$ = inst._bindListeners;
      for (var i=0, l=b$.length, info; (i<l) && (info=b$[i]); i++) {
        // Property listeners:
        // <node>.on.<property>-changed: <path]> = e.detail.value
        //console.log('[_setupBindListener]: [%s][%s] listening for [%s][%s-changed]', this.localName, info.path, info.id || info.index, info.property);
        //
        // TODO(sorvell): fix templatizer to support this before uncommenting
        // Optimization: only add bind listeners if the bound property is notifying...
        var node = inst._nodes[info.index];
        //var p = node._propertyInfo && node._propertyInfo[info.property];
        //if (node._prepParentProperties || !node._propertyInfo || (p && p.notify)) {
          this._addNotifyListener(node, inst, info.event, info.changedFn);
        //}
      }
    },

    // TODO(sorvell): note, adding these synchronously may impact performance,
    // measure and consider if we can defer until after first paint in some cases at least.
    _addNotifyListener: function(element, context, event, changedFn) {
      element.addEventListener(event, function(e) {
        return context._notifyListener(changedFn, e);
      });
    }
  };

</script>
